package engine;

import model.Model;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

/**
 * Agents are placed within this layer.
 *
 * <p>The Petri space is divided into regions for faster geo-access.</p>
 *
 * <p>Please beware of accessing and modifying the layer from different threads.
 * Set a synchronized block in the instance of this class.</p>
 *
 * @author Ricardo Cruz {@literal <ricardo.pdm.cruz@gmail.com>}
 * @since  2014-11-17
 */
public class LayerAgents
{
	/**
	 * As they move, the agents are split into regions of size, for faster geo
	 * access.
	 *
	 * <p>Please make sure it divides Petri width and height.</p>
	 */
	public static final int REGION_SIZE = 500;

	private class CellList extends LinkedList<Cell> {}
	private CellList geo_agents[][];
	private int count[];

	/**
	 * Create agents layer.
	 * @param nbrStates maximum agent's number of states -- we may want to move
	 * this to Model in the future
	 */
	public LayerAgents(int nbrStates) {
		geo_agents = new CellList [Model.getPetriWidth()/REGION_SIZE][Model.getPetriHeight()/REGION_SIZE];
		count = new int[nbrStates];
	}

	/**
	 * Retrieval of a random cell.
	 * @return cell
	 */
	public synchronized Cell getRandomCell() {
		Random r = new Random();
		while(true) {
			CellList cells = geo_agents[r.nextInt(geo_agents.length)][r.nextInt(geo_agents[0].length)];
			if(cells != null && cells.size() > 0)
				return cells.get(r.nextInt(cells.size()));
		}
	}

	/**
	 * Returns list of alive agents within the specified region.
	 *
	 * <p>Region coordinates are in um/500 units. So always divide your
	 * coordinates by 500 (or {@link REGION_SIZE}).</p>
	 *
	 * @param i region x-coordinate
	 * @param j region y-coordinate
	 * @return list of cells (can be null if empty, or out of range)
	 */
	public synchronized List<Cell> getCellsRegion(int i, int j) {
		if(i < 0 || j < 0 || i >= geo_agents.length || j >= geo_agents[0].length)
			return null;
		return geo_agents[i][j];
	}

	/**
	 * For every agent, calls diffuse process step.
	 */
	public synchronized void diffuse() {
		// we iterate using Iterator in order to be able to remove elements from
		// the currently iterative cycle
		for(int i = 0; i < geo_agents.length; i++)
			for(int j = 0; j < geo_agents[i].length; j++)
				if(geo_agents[i][j] != null)
					for(Iterator<Cell> it = geo_agents[i][j].iterator(); it.hasNext();) {
						Cell cell = it.next();
						cell.diffuse();
						int ni = (int)(cell.getX()/REGION_SIZE);
						int nj = (int)(cell.getY()/REGION_SIZE);
						if(i != ni || j != nj) {
							it.remove();
							if(ni < 0 || nj < 0 || ni >= geo_agents.length || nj >= geo_agents[0].length)
								continue;
							if(geo_agents[ni][nj] == null)
								geo_agents[ni][nj] = new CellList();
							geo_agents[ni][nj].add(cell);
						}
					}
	}

	/**
	 * The number of alive cells within the simulation.
	 * @return count of the several cell's states
	 */
	public synchronized int[] getCellCount() {
		return count;
	}

	/**
	 * Add cell agent to simulation.
	 * @param cell
	 */
	public synchronized void add(Cell cell) {
		count[cell.getCurrentState().getNbr()]++;
		int i = (int)(cell.getX()/REGION_SIZE);
		int j = (int)(cell.getY()/REGION_SIZE);
		if(i < 0 || j < 0 || i >= geo_agents.length || j >= geo_agents[i].length)
			return;  // discard it
		if(geo_agents[i][j] == null)
			geo_agents[i][j] = new CellList();
		geo_agents[i][j].add(cell);
	}

	/**
	 * Remove cell agent from simulation.
	 * @param cell
	 */
	public synchronized void remove(Cell cell) {
		count[cell.getCurrentState().getNbr()]--;
		int i = (int)(cell.getX()/REGION_SIZE);
		int j = (int)(cell.getY()/REGION_SIZE);
		if(i < 0 || j > 0 || i >= geo_agents.length || j >= geo_agents[i].length)
			return;  // not inside layer ?
		if(geo_agents[i][j] == null)
			geo_agents[i][j] = new CellList();
		geo_agents[i][j].remove(cell);
	}

	/**
	 * Notifies of cell state change. Also removes cell if newState is null.
	 * @param cell
	 * @param oldState
	 * @param newState
	 */
	public void cellStateChanged(State oldState, State newState) {
		count[oldState.getNbr()]--;
		count[newState.getNbr()]++;
	}
}
